<?php /*
==================================================
TextCMS -- simple plain/text CMS with GPL licence
(c) 2008, 4X_Pro
Visit us online: http://textcms.ru
==================================================
TODO:
+ получение базового каталога -- на уровень CMS
+ мультисайтовость
+ лог ошибок движка
лог ошибки 404
+ лог предупреждений
*/

define('TextCMS_version','0.96b');
define('TextCMS_multisite','1'); // указывает, нужно ли использовать мультисайтовый режим

global $pagedata;
global $status;
global $location;
global $sitepath;
global $exec_start;
global $lastmod;

if (!defined('TextCMS_Admin')) cms_main(); // если мы не в админ-режиме, вызываем главную функцию

/** Инициализиционная функция, настраивающая режимы работы скрипта **/
function cms_init() {
// инициализация и подготовительные операции
  $GLOBALS['TextCMS_exec_start']=array_sum(explode(' ',microtime())); // фиксируется время запуска скрипта
  error_reporting(E_ALL);
  $GLOBALS['TextCMS_debug']='';
  setlocale(LC_ALL,'ru_RU.UTF-8','ru_RU.utf-8','Russian_Russia.65001');

  set_error_handler('cms_error_handler');
  set_magic_quotes_runtime(0);
  if (get_magic_quotes_gpc()) cms_strip_arrays();	

  if (defined('TextCMS_multisite') && TextCMS_multisite) cms_getsite(); // определение текущего сайта
  if (!include(cms_basedir().'config.php')) {
    if (!include('files/config.def')) {
      header($_SERVER['SERVER_PROTOCOL'].' 500 Internal Server Error');
      echo 'Ошибка: файлы '.cms_basedir().'config.php и files/config.def повреждены, отсутствуют или недоступны для чтения!';
      exit();
    }
    else trigger_error('Ошибка: файл '.cms_basedir().'config.php поврежден, отсутствует или недоступен для чтения! Использован файл конфигурации по умолчанию! Если вы установили TextCMS впервые, то вам необходимо произвести начальную настройку сайта в разделе "Настройки", чтобы файл был создан.',E_USER_WARNING);
  }
  if (!isset($GLOBALS['config']['debug'])) $GLOBALS['config']['debug']=0;

  if (version_compare(phpversion(),'5.1.0','>=')) date_default_timezone_set('Europe/Moscow');
  if (isset($GLOBALS['config']['gzip']) && $GLOBALS['config']['gzip']) ob_start('ob_gzhandler',9);
  else ob_start(); // else enabling simple buffering
  $GLOBALS['TextCMS_JS']=array();
  $file=(defined('TextCMS_Admin')) ? 'admin.php' : __FILE__;
  $GLOBALS['sitepath']=preg_replace('|/+|','/',str_replace(basename($file),'',$_SERVER['SCRIPT_NAME']));
}

/** Определяет идентификатор сайта, к которому сделан запрос.
Сайт определяется по имени домена, к которому обратились.
Если домена нет в списке (файл /files/sites.txt), считается, что
обращение идет к сайту с идентификатором 0.
Идентификатор сайта влияет на имя каталога, из которого берутся данные для
отображения страницы, т.е. /files/site<идентификатор_сайта>/.
**/
function cms_getsite() {
  $GLOBALS['TextCMS_site']='0';
  $filename='files/sites.txt';
  if (file_exists($filename)) {
    $fh=fopen($filename,'r');
    if ($fh) {
      while ($buffer=fgets($fh)) if (strpos($buffer,':')!==false) {
        list($siteid,$domainlist)=explode(':',trim($buffer));
        if (strpos(','.$domainlist.',',','.strtolower($_SERVER['HTTP_HOST']).',')!==false) $GLOBALS['TextCMS_site']=$siteid;
      }
      fclose($fh);
    }
  }
}

/** Основная рабочая процедура **/
function cms_main() {
  cms_init();
// основная часть
  $GLOBALS['status']=200;
  $url=cms_url_parse();
  if ($GLOBALS['status']==200) {
    $GLOBALS['pagedata']=cms_page_info($url);
    $GLOBALS['lastmod']=$GLOBALS['pagedata']['lastmod'];
    $content='';
    if (is_array($GLOBALS['pagedata']) && cms_page_available($GLOBALS['pagedata'])) {
      $content=cms_page_load($GLOBALS['pagedata']['id'],false);
      if (isset($GLOBALS['config']['address'])) $content=str_replace('##ADDRESS#',$GLOBALS['config']['address'],$content);
      if (isset($GLOBALS['config']['phone'])) $content=str_replace('##PHONE#',$GLOBALS['config']['phone'],$content);
    }
    elseif (is_array($GLOBALS['pagedata']) && $GLOBALS['pagedata']['state']==1) {
      $GLOBALS['status']=410;
    }
    else {
      if ($GLOBALS['status']==200) $GLOBALS['status']=404;
    }
  }
  else $content='<span style="font-size: 150%">301</span> Эта страница перемещена на <a href="'.$GLOBALS['location'].'">'.$GLOBALS['location'].'</a>';
  cms_output($content);
  if ($GLOBALS['status']==404) cms_log_entry('err404',404,$_SERVER['PHP_SELF'],$url);
//  ob_flush();
}

/** Получение HURL страницы, запрошенной пользователем.
Поддерживаются 3 режима его получения. При использовании полноценного HURL
производится редирект с адресов, не использующих HURLs. **/
function cms_url_parse() {
  if ($GLOBALS['config']['hurls']==0) $url=isset($_REQUEST['url']) ? $_REQUEST['url'] : '/';
  elseif ($GLOBALS['config']['hurls']==1) {
    if (isset($_REQUEST['url'])) {
      $GLOBALS['status']=301;
      $GLOBALS['location']=cms_hurl($_REQUEST['url']);
    }
    if ($GLOBALS['sitepath']!='/') $url = str_replace($GLOBALS['sitepath'],'',$_SERVER['REQUEST_URI']);
    else $url = $_SERVER['REQUEST_URI'];
    $filename=basename(__FILE__);
    if (substr($url,0,strlen($filename)+1)==$filename.'/') {
      $GLOBALS['status']=301;
      $GLOBALS['location']=cms_hurl(str_replace($filename.'/','',$url));
    }
  }
  elseif ($GLOBALS['config']['hurls']==2) {
    $url = str_replace($GLOBALS['sitepath'],'',str_replace('/index.php/','/',$_SERVER['REQUEST_URI']));
  }
  return $url;
}

/** Построение HURL в соответствии с выбранным режимом запросов **/
function cms_hurl($page,$params='',$admin=false) {
  if ($GLOBALS['config']['hurls']==0 || $admin) {
    $url=$GLOBALS['sitepath'].'index.php?url='.$page;
    if ($params) $url.='&amp;';
  }
  elseif ($GLOBALS['config']['hurls']==1) {
    $url=preg_replace('|/+|','/',$GLOBALS['sitepath'].$page);
    if ($params) $url.='?'.$params;
  }
  elseif ($GLOBALS['config']['hurls']==2) {
    $url=$GLOBALS['sitepath'].'index.php'.$page;
    if ($params) $url.='?'.$params;
  }
  return $url;
}

/** Определение базового каталога для текущего сайта в files   **/
function cms_basedir() {
  if (!isset($GLOBALS['TextCMS_site'])) $GLOBALS['TextCMS_site']='0';
  return 'files/site'.$GLOBALS['TextCMS_site'].'/';
}

/** Поиск страницы с указанным URL или родительским URL, если страницу с текущим найти не удается **/
function cms_page_info($url,$error=true) {
  $pos=strpos($url,'?');
  if ($pos!==false) $url=substr($url,0,$pos); // отсекаем часть после ? из URL, так как в поиске страницы она участвовать не должна
   $filename=file_exists(cms_basedir().'content.txt') ? cms_basedir().'content.txt' : cms_basedir().'content.def';
  $fh=fopen($filename,'r');
  $result=false;
  if (!$fh) {
    trigger_error(E_USER_ERROR,'Не удалось открыть файл '.$filename.'!');
  }
  else {
    $url=preg_replace('|/+|','/','/'.$url); // решение проблемы с двойными / в URL и добавление / в начало (на всякий случай)
    $found=false;
    while(!$found && $buffer=fgets($fh)) { // полного парсинга не производится для ускорения процесса поиска, только выделяется первый элемент
      if (($pos=strpos($buffer,':'))!==false) {
        $cururl=substr($buffer,0,$pos);
        if ($cururl==$url || $cururl==$url.'/' || $cururl.'/'==$url) {
          $found=true;
          $result=$buffer;
          if ($cururl==$url.'/') {
            $GLOBALS['status']=301;
            $GLOBALS['location']=cms_hurl($url.'/');
          }
        }
        elseif (strpos($url,$cururl)===0) $result=$buffer;
      }
    }
  }
  fclose($fh);
  if ($result) {
    $result=cms_page_parse($result);
    if (!$found && $error) $GLOBALS['status']=404;
    $result['params']=substr($url,strlen($result['url'])); // та часть URL, которая не вошла в найденный, считается параметрами
  }
  return $result;
}

/** Парсинг строки с данными о странице в составе CMS.
Строка имеет такой формат:
url:id:template:status:lastmod:title
url -- HURL страницы
id -- порядковый номер страницы (он же -- имя файла с данными и параметрами)
template -- шаблон для страницы (если поле пусто, берется шаблон сайта по умолчанию)
state -- состояние страницы (0 -- нормальная, 1 -- удаленная, 2 -- черновик)
lastmod -- время последней модификации страницы (в формате Unix timestamp)
date -- дата (для новостей)
sort -- поле задания порядка сортировки 
title -- заголовок страницы
**/
function cms_page_parse($data) {
  $params=explode(':',trim($data),8);
  $parnames=array('url','id','template','state','lastmod','date','sort','title');
  $result=array();
  for ($i=0, $count=count($parnames); $i<$count; $i++) $result[$parnames[$i]]=$params[$i];
  return $result;
}

/** Формирование имени файла, содержащего страницу **/
function cms_page_filename($id,$draft=0) {
   $tmp = ($draft==2) ? '_draft' : '';
   return cms_basedir().'content/'.$id.$tmp.'.htm';
}

/** Подгрузка страницы с указанным id с диска, без какой-либо обработки **/
function cms_page_load($id,$draft=0) {
  if (!is_numeric($id)) {
    _dbg('Получен некорректный номер страницы: '.$id.'.');
    $GLOBALS['status']=404;
    return '';
  }
  $filename = cms_page_filename($id,$draft);
  if (!is_readable($filename)) {
    _dbg('Не удается найти или прочитать файл '.$filename);
    $GLOBALS['status']=404;
    return '';
  }
  // TODO: добавить лог обращений к несуществующим страницам
  return join('',file($filename));
}

/** Проверка того, что страница доступна пользователям для просмотра
(т.е. она имеет статус 0 или существует черновик (статус 2) и более старая публичная версия) **/
function cms_page_available($data) {
  if ($data['state']==0 || ($data['state']==2 && file_exists(cms_page_filename($data['id'],false)))) return true;
  else return false;
}

/** Загрузка модуля с указанным именем **/
function cms_module_load($name,$fatal=false) {
  $filename = 'modules/'.$name.'/module.php';
  $result=false;
  if (cms_valid_file($filename,true,true)) {
    if (include_once($filename)) $result=true;
    else trigger_error('При подгрузке модуля '.$name.' произошла ошибка!',($fatal) ? E_USER_ERROR : E_USER_WARNING);
  }
  elseif ($fatal) trigger_error('Не удается найти модуль '.$name.' или его имя содержит некорректные символы',E_USER_ERROR);
  return $result;
}

/** Вызов функции, находящейся в отдельном модуле **/
function cms_call($procname,$params=array()) {
  list($module)=explode('_',$procname);
  if ($module==$procname) $procname=$module.'_main';
  if (cms_module_load($module,false)) {
    if (!function_exists($procname)) {
      _dbg('Не удалось найти функцию '.$procname.', вызываемую из модуля '.$module.' директивой #block[]#. ');
      return false;
    }
    else return call_user_func_array($procname,$params);
  }
  else return false;
}

/** Обработка события: поиск файлов с именами функций по адресу events/имя_события/*.txt
и вызов соответствующих процедур**/
/*function cms_process_event($event,$params=array()) {
  if (!isset($GLOBALS['config']['noevents']) || !$GLOBALS['config']['noevents']) {
    $dirname='events/'.$event;
    if (cms_valid_file($dirname,false,false) && is_dir($dirname)) {
      $dh=opendir($dirname);
      while ($curfile=readdir($dh)) {
          if ($curfile!='.' && $curfile!='..') cms_call($event,$params);
      }
      closedir($dh);
    }
  }
}*/

// ==================================================================================
// Функции работы с шаблонами

/** Основная функция обработки шаблона **/
function cms_output($content,$admin_menu=false) {

  cms_output_parse($content);
  if ($GLOBALS['status']==404) $content = '<span style="font-size:150%">404</span> Страница не найдена';
  elseif ($GLOBALS['status']==410) $content = '<span style="font-size:150%">410</span> Страница удалена администратором сайта';
  if ($admin_menu!==false) $template=cms_output_load_template('admin.htm');
  else $template=cms_output_load_template('template.htm');
  cms_output_parse($template);
  if (!cms_ajax()) { // Для AJAX (точнее, AJAH) запросов выдаем только то, что сгенерировал модуль
    cms_output_parse_template($template);
    $template=str_replace('##CONTENT#',$content,$template);
    if ($admin_menu!==false) $template=str_replace('##ADMIN_MENU#',$admin_menu,$template);
    cms_output_parse_meta($template);
  }
  else $template=$content;
  if (cms_output_headers(strlen($template),md5(preg_replace('|<!--NOHASH-->(.*?)<!--/NOHASH-->|s','',$template)))) {
    print $template;
    ob_end_flush();
  }
}

/** Вывод HTTP-заголовков **/
function cms_output_headers($length=0,$hash='') {
  // настройка умолчаний, если они не были произведены ранее по каким-то причинам
  if (!isset($GLOBALS['status'])) $GLOBALS['status']=200; // по умолчанию статус равен 200
  if (!isset($GLOBALS['lastmod'])) $GLOBALS['lastmod']=time(); // по умолчанию дата последней модификации -- сейчас
  $hash='"'.$hash.'"';

  // проверка, можно ли взять страницу из кеша на стороне клиента
  $cache = true;
  if (isset($GLOBALS['config']['nocache']) && $GLOBALS['config']['nocache']==0
    && (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) || isset($_SERVER['HTTP_IF_NONE_MATCH']))) {
    if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { // проверяем If-Modified-Since
      $time = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
      if ($GLOBALS['lastmod']>$time) $cache=false; // если документ обновлялся после даты, указанной в кеше, не кешируем!
    }
    if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH']!='*') {
      if (strpos($_SERVER['HTTP_IF_NONE_MATCH'],',')===false) $matches=array($_SERVER['HTTP_IF_NONE_MATCH']);
      else $matches=explode(', ',$matches);
      if (!in_array($hash,$matches)) $cache=false;
    }
  } // если nocache не включен и есть либо If-Modified-Since, либо If-None-Match, выдаем из кеша
  else $cache=false;
  if ($cache && $GLOBALS['status']==200) { // статусы, отличающиеся от 200, не кешируем
    if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $hash==$_SERVER['HTTP_IF_NONE_MATCH']) $GLOBALS['status']=304;
  }

  // выдача статусов
  if ($GLOBALS['status']==204) {
    header($_SERVER['SERVER_PROTOCOL'].' 204 No Content');
    return false;
  }
  elseif ($GLOBALS['status']==304) {
    header($_SERVER['SERVER_PROTOCOL'].' 304 Not Modified');
    header('Last-Modified: '.date('r',$GLOBALS['lastmod']));
    if ($_SERVER['SERVER_PROTOCOL']=='HTTP/1.1') header('ETag:'.$hash); // для HTTP 1.1 выдаем ETag (MD5-хеш от страницы)
    return false;
  }
  elseif ($GLOBALS['status']==301) {
    header($_SERVER['SERVER_PROTOCOL'].'301 Moved Permanently');
    if (isset($GLOBALS['location'])) header('Location: '.$GLOBALS['location']);
    return false;
  }
  elseif ($GLOBALS['status']==302) {
    header($_SERVER['SERVER_PROTOCOL'].'302 Moved Temporary');
    if (isset($GLOBALS['location'])) header('Location: '.$GLOBALS['location']);
    return false;
  }
  elseif ($GLOBALS['status']==404) header($_SERVER['SERVER_PROTOCOL'].' 404 Not Found');
  elseif ($GLOBALS['status']==503) header($_SERVER['SERVER_PROTOCOL'].' 503 Service Unavailable');
  elseif ($GLOBALS['status']==410) header($_SERVER['SERVER_PROTOCOL'].' 410 Gone');
  else header($_SERVER['SERVER_PROTOCOL'].' 200 OK');

  header('Content-Type: text/html; charset='.$GLOBALS['config']['charset']);
  //if ($length) header('Content-length: '.$length);

  header('Last-Modified: '.date('r',$GLOBALS['lastmod']));
  if ($_SERVER['SERVER_PROTOCOL']=='HTTP/1.1') header('ETag:'.$hash); // для HTTP 1.1 выдаем ETag (MD5-хеш от страницы)
  if (strtoupper($_SERVER['REQUEST_METHOD'])=='HEAD') return false;
  return true;
}

/** Функция проверяет имя файла на наличие недопустимых символов и, при необходимости, существование
При $debug!=false информация об отсутствии файла сохраняется в отладочную информацию. **/
function cms_valid_file($filename,$exist=false,$debug=false) {
  $result=(substr($filename,0,1)!='/' && substr($filename,0,1)!='\\' && substr($filename,0,1)!='~');
  if ($result) {
    $test=array('..','://','`','\'','"',':',';',',','&','>','<');
    for ($i=0, $count=count($test); $i<$count && $result; $i++) $result=(strpos($filename,$test[$i])===false);
  }
  if ($result && $exist) {
    $result=is_readable($filename);
    if (!$result && $debug) _dbg('Файл '.$filename.' не найден или недоступен для чтения!');
  }
  return $result;
}

/** Загрузка шаблона
Загрузка шаблона происходит следующим образом:
1. Делается попытка загрузить файл из шаблона, указанного в параметре HTTP-запроса template, если он указан
2. Делается попытка загрузить файл из шаблона, указанного в $GLOBALS['pagedata']['template'], если он не пуст
3. Делается попытка загрузить файл из шаблона, выставленного в настройках сайта как основной
4. Делается попытка загрузить файл из шаблона default
5. Если ничего не получилось, то если имя файла == template.htm, выбрасываем ошибку 500,
иначе -- возвращаем пустую строку и заносим событие в отладочную переменную.
**/
function cms_output_load_template($name,$module=false) {
  if ($module) $name='modules/'.$module.'/'.$name;
  if (isset($_GET['template']) && cms_valid_file('template/'.$_GET['template'].'/'.$name,true,false)) {
    $filename='template/'.$_GET['template'].'/'.$name;
  }
  elseif (isset($GLOBALS['pagedata']['template']) && cms_valid_file('template/'.$GLOBALS['pagedata']['template'].'/'.$name,true,false)) {
    $filename='template/'.$GLOBALS['pagedata']['template'].'/'.$name;
  }
  elseif (isset($GLOBALS['config']) && isset($GLOBALS['config']['template']) && cms_valid_file('template/'.$GLOBALS['config']['template'].'/'.$name,true,false)) {
    $filename='template/'.$GLOBALS['config']['template'].'/'.$name;
    if ($name=='template.htm') $GLOBALS['pagedata']['template']=$GLOBALS['config']['template'];
  }
  elseif (cms_valid_file('template/default/'.$name,true,false)) {
    $filename='template/default/'.$name;
    if ($name=='template.htm') $GLOBALS['pagedata']['template']='default';
  }
  elseif ($name=='template.htm') {
    trigger_error('Не удается найти файл template.htm ни для текущего шаблона, ни для шаблона по умолчанию!',E_USER_ERROR);
  }
  else {
    trigger_error('Не удается найти файл шаблона '.$name,E_USER_WARNING);
    return '';
  }
  $filetime = filemtime($filename);
  $GLOBALS['lastmod']=max($GLOBALS['lastmod'],$filetime);
  return join('',file($filename));
}

/** Обработка директивы include. Выполняется прежде всех прочих и, в отличие от остальных, является рекурсивной.
**/
function cms_output_parse_include(&$template) {
  $count=preg_match_all('/##include\[([^\]]+?)\]#/s',$template,$matches);
  for ($i=0; $i<$count; $i++) {
    $params=explode($matches[1][$i],2);
    if (is_array($params) && count($params)>1) $newtmp=cms_output_load_template($params[0],$params[1]);
    else $newtmp=cms_output_load_template($matches[1][$i]);
    cms_output_parse_include($newtmp);
    $template=str_replace($matches[0][$i],$newtmp,$template);
  }
}

/** Общая процедура обработки шаблона.
Применяется как к контентной части шаблона, так и ко всему шаблону сайта
Шаблон может содержать директивы
##block[]# -- вствка блока, полученного в результате вызова функции с параметрами
##hurl[]# -- преобразование URL относит тещкуей страницы в HURL
##url[]# -- преобразование URL относительно текущей страницы в URL относительно корня сата
##uploaded[]# -- пока не реализовано
##include[]# -- вставка другого файла (выполняется прежде всех остальных)
**/
function cms_output_parse(&$template) {
  cms_output_parse_include($template);

  $template=str_replace('\\##','&#35;&#35;',$template);
  if (!isset($GLOBALS['config']['sitename'])) $GLOBALS['config']['sitename']='';
  $template=str_replace('##SITE_NAME#',$GLOBALS['config']['sitename'],$template);
  if (!isset($GLOBALS['pagedata']['title'])) $GLOBALS['pagedata']['title']='';
  $template=str_replace('##PAGE_NAME#',$GLOBALS['pagedata']['title'],$template);

  $count=preg_match_all('/##url\[([^\]]+?)\]#/s',$template,$matches);
  for ($i=0; $i<$count; $i++) {
    $params=explode(',',$matches[1][$i],2);
    if (is_array($params) && count($params)>1) $newtmp=$GLOBALS['sitepath'].$params[0].'?'.$params[1];
    else $newtmp=$GLOBALS['sitepath'].$params[0];
    $template=str_replace($matches[0][$i],$newtmp,$template);
  }

  $count=preg_match_all('/##hurl\[([^\]]+?)\]#/s',$template,$matches);
  for ($i=0; $i<$count; $i++) {
    $params=explode(',',$matches[1][$i],2);
    if (is_array($params) && count($params)>1) $newtmp=cms_hurl($params[0],$params[1]);
    else $newtmp=cms_hurl($params[0]);
    $template=str_replace($matches[0][$i],$newtmp,$template);
  }

  $count=preg_match_all('/##block\[([^\]]+?)\]#/s',$template,$matches);
  for ($i=0; $i<$count; $i++) {
    $params=explode(',',$matches[1][$i]);
    $funcname=$params[0];
    array_shift($params);
    $newtmp=cms_call($funcname,$params);
    if ($newtmp!==false) $template=str_replace($matches[0][$i],$newtmp,$template);
  }
}

/** Обработка шаблона, применяемая только к общесайтовому шаблону.
Именно здесь делается вставка подключения CSS, вывод отладочной информации и т.п. **/
function cms_output_parse_template(&$template) {
  $replace = array();
  $replace['##ADDRESS#']=isset($GLOBALS['config']['address']) ? $GLOBALS['config']['address'] : '';
  $replace['##PHONE#']=isset($GLOBALS['config']['phone']) ? $GLOBALS['config']['phone'] : '';
  if (!cms_licence_check()) $replace['<!--##COPYRIGHT#-->']='Сайт работает на <a href="http://textcms.ru">TextCMS</a>. &copy; 2008&ndash;2010, 4X_Pro.';
  if ($GLOBALS['config']['debug']!=0 && $GLOBALS['TextCMS_debug']!='') {
    $replace['<!--##DEBUG#-->']='<!--NOHASH--><div style="font-size: 0.8em; padding: 4px; border: #4C4 1px solid; margin: 4px">'.
    $GLOBALS['TextCMS_debug'].'</div><!--NOHASH-->';
  }
  $replace['<!--##CSS#-->']='<link href="'.$GLOBALS['sitepath'].'template/'.$GLOBALS['pagedata']['template'].'/style.css" type="text/css" rel="stylesheet"></link>';
  $replace['<!--##JAVASCRIPT#-->']='';
  if (isset($GLOBALS['TextCMS_JS']) && is_array($GLOBALS['TextCMS_JS'])) {
    foreach ($GLOBALS['TextCMS_JS'] as $curjs) $replace['<!--##JAVASCRIPT#-->'].='<script type="text/javascript" src="'.$GLOBALS['sitepath'].'js/'.$curjs.'"></script>';
  }

  if (strpos($template,'<!--##STATS#-->')!==false) {
    $replace['<!--##STATS#-->']='<!--NOHASH-->Время выполнения скрипта: '.sprintf("%.3f",(array_sum(explode(' ',microtime()))-$GLOBALS['TextCMS_exec_start'])).
    ' сек. Модулей подключено: '.(count(get_included_files())).'.';
    if (function_exists('memory_get_usage')) $replace['<!--##STATS#-->'].=' Памяти использовано: '.memory_get_usage().' байт.';
    $replace['<!--##STATS#-->'].='<!--/NOHASH-->';
  }

  foreach ($replace as $key=>$value) {
    if (strpos($template,$key)!==false) $template=str_replace($key,$value,$template);
    elseif ($key=='<!--##DEBUG#-->' || $key=='<!--##COPYRIGHT#-->') {
      if (strpos($template,'</body')!==false) $template=str_replace('</body',$value.'</body',$template);
      elseif (strpos($template,'</html')!==false) $template=str_replace('</html',$value.'</html',$template);
      else $template.=$value;
    }
  }
}

/** Вставка мета-тегов при наличии конструкций вида #meta[имя,значение]# **/
function cms_output_parse_meta(&$template) {
  $count=preg_match_all('|##meta\[([^,]+?),(.*?)\]#\r?\n?|is',$template,$meta);
  $metastring='';
  for ($i=0; $i<$count; $i++) {
    $metastring.='<meta name="'.htmlspecialchars($meta[1][$i]).'" content="'.htmlspecialchars(trim($meta[2][$i])).'" />'."\n";
    $template=str_replace($meta[0][$i],'',$template);
  }
  if (preg_match('|</head>|s',$template)) $template=preg_replace('|</head>|s',$metastring.'</head>',$template);
  else $template=preg_replace('|<body>|s',$metastring.'<body>',$template);
}

// ==================================================================================
// Вспомогательные функции

/** Удаление экранирующих \ из массивов с исходными данными, если включены magic_quotes **/
function cms_strip_arrays() {
  $_GET=&str_replace(array('\\"','\\\'','\\`','\\\\'),array('"','\'','`','\\'),$_GET);
  $_POST=&str_replace(array('\\"','\\\'','\\`','\\\\'),array('"','\'','`','\\'),$_POST);
  $_COOKIE=&str_replace(array('\\"','\\\'','\\`','\\\\'),array('"','\'','`','\\'),$_COOKIE);
}

function cms_licence_check() {
  return false;
}

/** Занесение в лог различных событий. Лог ведется в CSV-формате, 
что удобно для импорта в OpenOffice Calc или его аналог Microsoft Excel.
Формат лога:
Дата(Г-м-д час:мин),код,URI,Referer,файл,резеррв,IP,HTTP_X_FORWARDED_FOR,текст_ошибки **/
function cms_log_entry($log,$code,$file,$text) {
	$buffer=date('Y-m-d h:i').','.$code.','.str_replace(',','\\,',$_SERVER['REQUEST_URI']).','.
	str_replace(',','\\,',isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '').','.$file.', ,'.
		$_SERVER['REMOTE_ADDR'].','.str_replace(',','\\,',isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : '').
		','.str_replace(',','\\,',$text)."\n";
	$filename='logs/'.$log.'.csv';
	if (cms_valid_file($filename)) {
		$fh=fopen($filename,'a+');
		if ($fh) {
			fputs($fh,$buffer);
			fclose($fh);
		}
		else _dbg('Не удалось сохранить событие в лог '.$log.'.csv!');
	}
	else _dbg('Некорректное имя лог-файла '.$log.'.csv!');
}

/** Обработчик ошибок. Выдает статус 500 и выводит дружественное сообщение об ошибке **/
function cms_error_handler($errno, $errstr, $errfile, $errline) {
  static $callnumber;

  $debug=debug_backtrace();
  $errstr=str_replace("\t",' ',$errstr);
  $filemsg=$errno."\t".$errstr."\t".$errline."\t";
  $errmsg='<p>'.$errstr.' (строка '.$errline.', '.$errfile.', ошибка: '.$errno.')'.'</p><ul style="font-size: 0.9em; color: #600">';
  $count=count($debug);
  for ($i=1; $i<$count; $i++) {
    $errmsg.='<li>'.$debug[$i]['function'].'()'.
    ' &mdash; '.((isset($debug[$i]['file'])) ? $debug[$i]['file'] : 'неизвестный файл').', '.
    'строка '.((isset($debug[$i]['line'])) ? $debug[$i]['line'] : 'неизвестна').'</li>'; //.var_dump($debug[$i]['args'])
  }
  $filemsg.=$debug[1]['function'].' '.
  (isset($debug[1]) && (isset($debug[1]['line'])) ? $debug[1]['line'] : 'unknown')."\t".
  (isset($debug[1]) && (isset($debug[1]['file'])) ? $debug[1]['file'] : 'unknown')."\t";
  $errmsg.='</ul>';

  if (($errno & E_ERROR) || ($errno & E_USER_ERROR)) {
		cms_log_entry('error',$errno,$errfile,$errstr);
    if (!headers_sent() && $callnumber==0) {
      header($_SERVER['SERVER_PROTOCOL'].' 500 Internal Server Error');
      header('Content-Type: text/html; charset=utf-8');
      echo '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
      "http://www.w3.org/TR/html4/loose.dtd">
      <head>
      <title>Ошибка сайта</title>
      <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
      </head><body>';
    }
    $callnumber++;
    if (!defined('CONFIG_ADMIN_ADDRESS')) define('CONFIG_ADMIN_ADDRESS','unknown');

    $email = isset($GLOBALS['config']['admin_email']) ? $GLOBALS['config']['admin_email'] : '';
    echo '<div style="font-size: 1em; padding: 4px; font-weight: bold; color: #C44; border: #C00 1px solid; margin: 4px">
    На сайте произошла ошибка. Пожалуйста, сообщите о ней администратору сайта по адресу <a href="mailto:'.$email.'">'.$email.'</a> ';
    if (isset($GLOBALS['config']['debug']) && $GLOBALS['config']['debug']!=0) {
      echo '<p>'.$errmsg.'</p>';
      echo 'Вспомогательная отладочная информация: '.$GLOBALS['TextCMS_debug'];
    }
    echo '</div></body></html>';
    ob_end_flush();
    exit();
  }
  else {
		_dbg(str_replace('<p>','',str_replace('</p>','',$errmsg)));
		if (isset($GLOBALS['config']['debug']) && $GLOBALS['config']['debug']!=0) cms_log_entry('warn',$errno,$errfile,$errstr);
	}
}

/** Проверка, является ли данный запрос AJAX-запросом или нет **/
function cms_ajax() {
  return (isset($_REQUEST['ajax']));
}

/** Добавление подключаемого файла JavaScipt **/
function cms_jscript($script) {
  if (is_array($script)) $GLOBALS['TextCMS_JS']=array_merge($GLOBALS['TextCMS_JS'],$script);
  else {
    $params=func_get_args();
    $GLOBALS['TextCMS_JS']=array_merge($GLOBALS['TextCMS_JS'],$params);
  }
}

/** Помещение информации в отладочный массив **/
function _dbg() {
  if (isset($GLOBALS['config']['debug']) && $GLOBALS['config']['debug']!=0) {
    if (!isset($GLOBALS['TextCMS_debug'])) $GLOBALS['TextCMS_debug']='';
    $GLOBALS['TextCMS_debug'].='<p>';
    foreach (func_get_args() as $name=>$value) {
      if (is_array($value) || is_object($value)) $GLOBALS['TextCMS_debug'].=$name.': '.nl2br(str_replace('  ','&nbsp;',print_r($value,true)));
      else $GLOBALS['TextCMS_debug'].=$name.': '.htmlspecialchars($value).' ';
    }
    $GLOBALS['TextCMS_debug'].="</p>\n";
  }
}
